package jmine.tec.utils.parsers;

import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.io.UnsupportedEncodingException;
import java.text.DateFormat;
import java.text.NumberFormat;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.StringTokenizer;

/**
 * @author hugo
 */
public class CSVReader implements Iterable<CSVRow> {

    public static final String ISO_8859_1 = "ISO-8859-1";

    private final char lineSeparator;

    private final char columnSeparator;

    private DateFormat dateFormat;

    private final BufferedReader source;

    private final List<CSVRow> matrix = new ArrayList<CSVRow>();

    private NumberFormat numberFormat;

    private boolean isAllRead = false;

    /**
     * C'tor.
     * 
     * @param lineSeparator The character to be used as a line separator
     * @param columnSeparator The character to be used as a column separator
     * @param numberFormat The number format to be used to parse double values
     * @param formatter The Date formatter to be used to parse dates (if null, a SimpleDateFormat with the system's locale will be used)
     * @param input InputStream from which the reader must read
     * @param charset charset to be used to read data
     * @throws UnsupportedEncodingException Lançado se o charset não for suportado
     */
    public CSVReader(char lineSeparator, char columnSeparator, NumberFormat numberFormat, DateFormat formatter, InputStream input,
            String charset) throws UnsupportedEncodingException {
        this(lineSeparator, columnSeparator, numberFormat, formatter, input == null ? null : new InputStreamReader(input, charset));
    }

    /**
     * C'tor.
     * 
     * @param lineSeparator The character to be used as a line separator
     * @param columnSeparator The character to be used as a column separator
     * @param numberFormat The number format to be used to parse double values
     * @param formatter The Date formatter to be used to parse dates (if null, a SimpleDateFormat with the system's locale will be used)
     * @param input InputStream from which the reader must read
     * @param charset charset to be used to read data
     * @throws UnsupportedEncodingException Lançado se o charset não for suportado
     */
    public CSVReader(char lineSeparator, char columnSeparator, NumberFormat numberFormat, DateFormat formatter, byte[] input, String charset)
            throws UnsupportedEncodingException {
        this(lineSeparator, columnSeparator, numberFormat, formatter, input == null ? null : new ByteArrayInputStream(input), charset);
    }

    /**
     * C'tor.
     * 
     * @param lineSeparator The character to be used as a line separator
     * @param columnSeparator The character to be used as a column separator
     * @param numberFormat The number format to be used to parse double values (if null, a NumberFormat SHORT styled with the US Locale will
     * be used)
     * @param formatter The Date formatter to be used to parse dates (if null, a DateFormat SHORT styled with the US Locale will be used)
     * @param reader the {@link Reader} from which the reader must read
     */
    public CSVReader(char lineSeparator, char columnSeparator, NumberFormat numberFormat, DateFormat formatter, Reader reader) {
        Locale defaultLocale = Locale.US;

        this.lineSeparator = lineSeparator;
        this.columnSeparator = columnSeparator;
        this.dateFormat = formatter;
        if (this.dateFormat == null) {
            this.dateFormat = DateFormat.getDateInstance(DateFormat.SHORT, defaultLocale);
        }
        this.numberFormat = numberFormat;
        if (this.numberFormat == null) {
            this.numberFormat = NumberFormat.getInstance(defaultLocale);
        }
        this.source = new BufferedReader(reader);
    }

    /**
     * C'tor. Assumes the line separator is '\n' and that the date formatter is a SimpleDateFormat (shouldn't be used if not specified).
     * 
     * @param columnSeparator The character to be used as a column separator
     * @param reader the {@link Reader} from which the reader must read
     */
    public CSVReader(char columnSeparator, Reader reader) {
        this('\n', columnSeparator, null, null, reader);
    }

    /**
     * Retorna o numero da linha.
     * 
     * @param lineNumber numero da linha.
     * @return {@link CSVRow}
     * @throws IOException exception.
     */
    public CSVRow getLine(int lineNumber) throws IOException {
        this.loadLine(lineNumber);
        return this.matrix.get(lineNumber);
    }

    /**
     * @return the columnSeparator
     */
    public char getColumnSeparator() {
        return this.columnSeparator;
    }

    /**
     * @return the lineSeparator
     */
    public char getLineSeparator() {
        return this.lineSeparator;
    }

    /**
     * Carrega o arquivo no reader.
     * 
     * @param i numero da linha.
     * @return se a linha pode ser carregada
     */
    private boolean loadLine(int i) {
        List<String> linha = new ArrayList<String>();
        // A linha já esta carregada.
        if (i < this.matrix.size()) {
            return true;
        }
        if (this.isAllRead) {
            return false;
        }
        String line;
        try {
            line = this.source.readLine();
            if (line == null) {
                this.isAllRead = true;
                return false;
            }
        } catch (IOException e) {
            e.printStackTrace();
            this.isAllRead = true;
            return false;
        }
        StringTokenizer tokenizer = new StringTokenizer(line, String.valueOf(this.columnSeparator));
        int index = 0;
        while (tokenizer.hasMoreTokens()) {
            String token = tokenizer.nextToken();
            linha.add(token);
            index += token.length() + 1;
            while ((index < line.length()) && (line.charAt(index) == this.columnSeparator)) {
                linha.add("");
                index++;
            }
        }
        this.matrix.add(new CSVRow(linha, this.dateFormat, this.numberFormat, this.matrix.size(), line));
        return true;
    }

    /**
     * Retorna o numero total de linhas do arquivo. Inicializa a matriz (lazy).
     * 
     * @param lineNumber linha.
     * @return se esta linha existe.
     */
    private boolean hasNext(int lineNumber) {
        return this.loadLine(lineNumber);
    }

    /**
     * {@inheritDoc}
     */
    public Iterator<CSVRow> iterator() {
        return new CSVRowIterator();
    }

    /**
     * Iterator de linhas CSV.
     * 
     * @author wagner
     */
    private class CSVRowIterator implements Iterator<CSVRow> {
        private int index = 0;

        /**
         * {@inheritDoc}
         */
        public boolean hasNext() {
            return CSVReader.this.hasNext(this.index);
        }

        /**
         * {@inheritDoc}
         */
        public CSVRow next() {
            if (this.hasNext()) {
                try {
                    return CSVReader.this.getLine(this.index++);
                } catch (IOException e) {
                    e.printStackTrace();
                    return null;
                }
            } else {
                return null;
            }
        }

        /**
         * {@inheritDoc}
         */
        public void remove() {
            throw new UnsupportedOperationException();
        }

    }
}
